package roborally.supporting;

import exception.IllegalConvertionException;
import exception.NegativeAmountException;
import roborally.factory.PhysicalAmountFactory;
import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Immutable;
import be.kuleuven.cs.som.annotate.Raw;
import be.kuleuven.cs.som.annotate.Value;

/**
 * A value class representing an physical amount, consisting of a number and a unit.
 * 
 * @invar The amount must be a legal value.
 * 		| isValidAmount(this.getPhysicalAmount())
 * @invar The unit of this must be a valid unit.
 * 		| isValidUnit(this.getUnit())
 * @note This class has a natural ordering that is inconsistent with equals.
 * 
 * @author Molderez Tom, 2de bach Ingenieurswetenschappen Computerwetenschappen-Elektrotechniek and Reniers Jorn, 2de bach Ingenieurswetenschappen Werktuigkunde-Computerwetenschappen
 * @version 2.0
 *
 */
@Value
public class PhysicalAmount implements Comparable<PhysicalAmount> {

//Constructors -------------------------------------------------------------------------------------------------------------------	
	
	/**
	 * Creates a new physicalAmount. 
	 * 
	 * @param amount The amount of the new physicalAmount.
	 * @param unit The unit of the new physicalAmount.
	 * @post The amount of the new physicalAmount will be amount if it is a valid amount.
	 * 		| if PhysicalAmount.isValidAmount(amount)
	 * 		| 	then (new this).getAmount() == amount
	 * @post If the unit is valid, the unit of the new physicalAmount will be unit.
	 * 		| if PhysicalAmount.isValidEnergyUnit(unit)
	 * 		|	then (new this).getUnit() == unit
	 * @throws IllegalArgumentException
	 * 		If the unit is not a valid unit or if the amount is not valid.
	 * 		| !canHaveAsUnit(unit) || !canHaveAsAmount(amount)
	 * @note Use the factory to create a new physicalAmount. In that case, recourses are not
	 * spend useless by creating the same amount multiple times.
	 */
	public PhysicalAmount(double amount, Unit unit) throws IllegalArgumentException {
		if(isValidAmount(amount))
			this.amount = amount;
		else throw new IllegalArgumentException("Illegal amount.");
		if(isValidUnit(unit))
			this.unit = unit;
		else throw new IllegalArgumentException("Illegal energy unit.");
	}
	
	/**
	 * A constant representing zero energy.
	 */
	public static final PhysicalAmount ENERGY_ZERO = PhysicalAmountFactory.get().create(0D, Unit.WATTSECOND);

	/**
	 * A constant representing zero weight.
	 */
	public static final PhysicalAmount WEIGHT_ZERO = PhysicalAmountFactory.get().create(0D, Unit.GRAM);
	
	/**
	 * Checks the amount.
	 * 
	 * @param amount The amount to be checked.
	 * @return
	 * 		| if amount < 0
	 * 		|	then result == false
	 */
	@Raw
	public static boolean isValidAmount(double amount) {
		return amount >= 0;
	}

	/**
	 * Returns the amount of this physicalAmount.
	 */
	@Basic
	@Raw
	@Immutable
	public double getAmount() {
		return this.amount;
	}

	/**
	 * A variable representing the (numerical) amount of this physicalAmount.
	 */
	private final double amount;

	/**
	 * Checks the unit.
	 * 
	 * @param unit The unit to be checked.
	 * @return False if the unit is null.
	 * 		| if unit == null
	 * 		|	then result == false
	 */
	@Raw
	public static boolean isValidUnit(Unit unit) {
		return unit != null;
	}

	/**
	 * Returns the unit of this physicalAmount.
	 */
	@Basic
	@Raw
	@Immutable
	public Unit getUnit() {
		return this.unit;
	}

	/**
	 * A variable representing the unit of this physicalAmount.
	 */
	private final Unit unit;

	/**
	 * Returns a hashcode of this physicalAmount according to the Java API specifications.
	 * 
	 * @return An integer equal to the sum of the amount of this physicalAmount modulo the maximum integer and the hashcode of the unit of this physicalAmount.
	 * 		| result == ( ((int) (this.getAmount() % Integer.MAX_VALUE)) + this.getUnit().getSymbol().hashCode() )
	 */
	@Override
	public int hashCode() {
		return ( ((int) (this.getAmount() % Integer.MAX_VALUE)) + this.getUnit().getSymbol().hashCode() );
	}

	/**
	 * Throws an exception whenever a method tries to clone a given physicalAmount.
	 * 
	 * @throws CloneNotSupportedException
	 * 		Every time this method should be evoked.
	 * 		| true
	 */
	@Override
	public PhysicalAmount clone() throws CloneNotSupportedException {
		throw new CloneNotSupportedException("PhysicalAmounts can not be cloned.");
	}

	/**
	 * Adds the given physicalAmounts  to this.
	 * 
	 * @param physicalAmounts The physicalAmounts to be added.
	 * @pre The physicalAmount can't be null.
	 * 		| for each physicalAmount in {physicalAmounts}
	 * 		|	physicalAmount != null
	 * @return If they have equivalent unit, the result is an physicalAmount with as amount the sum of the amounts and as unit the one of this.
	 * 			Else they are first converted to the unit of this.
	 * 		| let amount = 0
	 * 		| for each physicalAmount in {physicalAmounts}
	 * 		|	amount += physicalAmount.convertTo(this.getUnit()).getAmount()
	 * 		| then result == PhysicalAmountFactory.getInstance().new(this.getAmount() + amount, this.getUnit())
	 * @throws IllegalConvertionException
	 * 		| if for some physicalAmount in {physicalAmounts} !physicalAmount.getUnit().canConvertTo(this.getUnit())
	 */
	public PhysicalAmount add(PhysicalAmount... physicalAmounts)
			throws IllegalConvertionException {
				Double amount = 0D;
				for(PhysicalAmount lusPhysicalAmount : physicalAmounts) {
					assert lusPhysicalAmount != null;
					amount += lusPhysicalAmount.convertTo(this.getUnit()).getAmount();
				}
				return PhysicalAmountFactory.get().create(this.getAmount() + amount, this.getUnit());
			}

	/**
	 * Subtracts the given PhysicalAmounts from this PhysicalAmount.
	 * 
	 * @param physicalAmounts The PhysicalAmounts to be subtracted.
	 * @pre The PhysicalAmount can't be null.
	 * 		| for each physicalAmount in {physicalAmounts}
	 * 		|	physicalAmount != null
	 * @return
	 * 		| let temp = PhysicalAmountFactory.get().create(0D, this.getUnit()).add(physicalAmounts)
	 * 		| result == PhysicalAmountFactory.get().create(this.getAmount() - temp.getAmount(), this.getUnit())
	 * @throws IllegalConvertionException
	 * 		| if for some physicalAmount in {physicalAmounts} !physicalAmount.getUnit().canConvertTo(this.getUnit())
	 * @throws NegativeAmountException
	 * 		| let temp = PhysicalAmountFactory.get().create(0D, this.getUnit()).add(physicalAmounts)
	 * 		| if this.getAmount() - temp.getAmount() < 0
	 */
	public PhysicalAmount subtract(PhysicalAmount... physicalAmounts)
			throws IllegalArgumentException, NegativeAmountException {
		PhysicalAmount temp = PhysicalAmountFactory.get().create(0D, this.getUnit()).add(physicalAmounts);
		if(this.getAmount() - temp.getAmount() <0)
			throw new NegativeAmountException(this.getAmount() - temp.getAmount(), "Negative Amount in subtraction.");
		return PhysicalAmountFactory.get().create(this.getAmount() - temp.getAmount(), this.getUnit());
	}

	/**
	 * Multiplies the amount of this PhysicalAmount by the absolute value of the given factor.
	 * 
	 * @param factor The double with which this amount should be multiplied.
	 * @return A PhysicalAmount with the same unit but the amount is multiplied by the absolute value of the given factor.
	 * 		| result ==  PhysicalAmountFactory.getInstance().newPhysicalAmount(this.getAmount()*Math.abs(factor), this.getUnit())
	 */
	public PhysicalAmount multiply(double factor) {
		return PhysicalAmountFactory.get().create(this.getAmount()*Math.abs(factor), this.getUnit());
	}

	/**
	 * Divides the amount of this PhysicalAmount by the given factor.
	 * 
	 * @param factor The double with which this amount should be divided.
	 * @return A PhysicalAmount with the same unit but the amount is divided by the absolute value of the given factor.
	 * 		| result ==  PhysicalAmountFactory.getInstance().newPhysicalAmount(this.getAmount()/Math.abs(factor), this.getUnit())
	 * @throws IllegalArgumentException
	 * 		| if factor == 0
	 */
	public PhysicalAmount division(double factor) throws IllegalArgumentException{
		if (factor == 0)
			throw new IllegalArgumentException("Division by zero is not allowed.");
		return PhysicalAmountFactory.get().create(this.getAmount() / Math.abs(factor), this.getUnit());
	}
	
	/**
	 * Converts this to the given unit if valid.
	 * 
	 * @param unit The unit to convert to.
	 * @return
	 * 		| PhysicalAmountFactory.get().create(this.getAmount() * this.getUnit().getConvertionFactor(unit), unit)
	 * @throws IllegalArgumentException
	 * 		| unit == null
	 * @throws IllegalConvertionException
	 * 		| !this.getUnit().canConvertTo(unit)
	 */
	public PhysicalAmount convertTo(Unit unit) throws IllegalArgumentException, IllegalConvertionException {
		if(unit == null)
			throw new IllegalArgumentException("Cannot convert to null");
		return PhysicalAmountFactory.get().create(this.getAmount() * this.getUnit().getConvertionFactor(unit), unit);
	}
	
	/**
	 * Compares this with physicalAmount according to the comparable interface of java.
	 * 
	 * @param physicalAmount The PhysicalAmount to compare this with.
	 * @return Converts the given amount to the unit of this, and then compares the amounts.
	 * 			If this amount is greater the result will be 1,
	 * 			if the amounts are equal, the result is 0 and if the other amount is greater the result will be -1.
	 * 		| let temp be physicalAmount.convertTo(this.getUnit())
	 * 		| if this.getAmount() > temp.getAmount()
	 * 		|	then result == 1
	 * 		| if this.getAmount() == temp.getAmount()
	 * 		|	then result == 0
	 * 		| else result == -1
	 * @throws IllegalArgumentException
	 * 		If physicalAmount is null.
	 * 		| physicalAmount == null
	 * @throws IllegalConvertionException
	 * 		If the units can not convert to each other.
	 * 		| !physicalAmount.getUnit().canConvertTo(this.getUnit())
	 */
	@Override
	public int compareTo(PhysicalAmount physicalAmount) throws NullPointerException, IllegalConvertionException {
		if(physicalAmount == null)
			throw new IllegalArgumentException("Cannot compare to null");
		physicalAmount = physicalAmount.convertTo(this.getUnit());
		if (this.getAmount() > physicalAmount.getAmount())
			return 1;
		if (this.getAmount() == physicalAmount.getAmount())
			return 0;
		return -1;			
	}
	
	/**
	 * Returns physicalAmount as a fraction of this PhysicalAmount. (physicalAmount/this)
	 * 
	 * @param physicalAmount The physicalAmount that must be divided by this amount.
	 * @return The given physicalAmount is converted to the unit of this 
	 * 			and then the result is the amount of physicalAmount divided by the amount of this.
	 * 		| physicalAmount = physicalAmount.convertTo(this.getUnit())
	 * 		| result == physicalAmount.getAmount()/this.getAmount()
	 * @throws IllegalArgumentException
	 * 		| If energy is null or the amount of energy is 0 or this and energy have different units.
	 * 		| physicalAmount==null || physicalAmount.getAmount()==0
	 * @throws IllegalConvertionException
	 * 		If the units can not convert to each other.
	 * 		| !physicalAmount.getUnit().canConvertTo(this.getUnit()) 
	 * 
	 * @note this will give an exact result. If it must be displayed it would be better to round off a bit. 
	 * This should be done in the class that is responsible for the interaction with the GUI, Facade.
	 */
	public double getAsFractionOfThis(PhysicalAmount physicalAmount) throws IllegalArgumentException, IllegalConvertionException{
		if(physicalAmount==null)
			throw new IllegalArgumentException("Null is not allowed.");
		if(this.getAmount()==0)
			throw new IllegalArgumentException("Division by zero is not allowed.");
		physicalAmount = physicalAmount.convertTo(this.getUnit());
		return physicalAmount.getAmount()/this.getAmount();
	}  
	
	/**
	 * Checks whether the given object equals this PhysicalAmount.
	 * 
	 * @param object The object to compare this one to.
	 * @return True if and only if the object is an PhysicalAmount and if the amounts are the same and if the units are the same.
	 * 		| if object == null 
	 * 		|	then result == false
	 * 		| if PhysicalAmount.class == object.getClass()
	 * 		|	then result == PhysicalAmount.class.cast(object).getAmount() == this.getAmount() && PhysicalAmount.class.cast(object).getUnit().equals(this.getUnit()
	 * 		| else
	 * 		| 	result == false
	 * @note This class has a natural ordering that is inconsistent with equals.
	 */
	@Override
	public boolean equals(Object object){
		if(object == null) 
			return false;
		if(PhysicalAmount.class == object.getClass()) 
			return PhysicalAmount.class.cast(object).getAmount() == this.getAmount() && PhysicalAmount.class.cast(object).getUnit().equals(this.getUnit());
		else
			return false;
	}
	
	/**
	 * Returns a textual representation of this physicalAmount.
	 * @return A textual representation.
	 * 		| result == "Phys: [" + this.getAmount() + " " + this.getUnit() + "]"
	 */
	@Override
	public String toString() {
		return "Phys: [" + this.getAmount() + " " + this.getUnit() + "]";
	}

}